package ro.upt.ac.cstaicu.execution;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;

import ro.upt.ac.cstaicu.analyze.Entity;
import ro.upt.ac.cstaicu.instrumentation.Instrumentor;
import ro.upt.ac.cstaicu.instrumentation.Predicate;
import ro.upt.ac.cstaicu.instrumentation.logging.GlobalLogger;

/**
 * A class loader that loads dynamically the classes in the analyzed project. It
 * inherits {@link ClassLoader}
 * 
 * @author Cristian-Alexandru STAICU
 * 
 */
public class CustomClassLoader extends ClassLoader {

	/**
	 * Path to the analyzed project
	 */
	private String projectClassPath;
	
	private static ArrayList<ImprovedJarReader> jarsInCP = new ArrayList<ImprovedJarReader>();

	private ClassLoader parentCL;

	private Instrumentor instrumentationModule;

	private GlobalLogger logger;

	private Collection<String> ignoredTests;

	private HashMap<String, Class<?>> history = new HashMap<String, Class<?>>();
	private static String lastProject = "";
	/**
	 * Initialize the project path
	 * 
	 * @param projectClassPath
	 *            Path to the analyzed project
	 * @param classLoader 
	 * @param logger 
	 * @param instrumentationModule 
	 */
	public CustomClassLoader(File[] classPathEntries, String projectClassPath, ClassLoader classLoader, Instrumentor instrumentationModule, GlobalLogger logger, Collection<String> ignoredTests) {
		this.ignoredTests = ignoredTests;
		ignoredTests.add("ro.upt.ac.cstaicu.instrumentation.logging.PredicateCounterLogger");
		this.parentCL = classLoader;
		this.projectClassPath = projectClassPath;
		this.instrumentationModule = instrumentationModule;
		this.logger = logger;
		if (lastProject != projectClassPath) {
			jarsInCP.clear();
//			lastProject = projectClassPath;
			if (classPathEntries != null)
				for (File f : classPathEntries) {
					try { 						
						jarsInCP.add(new ImprovedJarReader(f.getPath()));
					} catch (Exception e) {
						//skip file
					}	
				}
		}
	}

	/**
	 * This method is trying to load the desired class first using the parent
	 * {@link ClassLoader} otherwise it loads it from the project path.
	 */
	public java.lang.Class<?> loadClass(String name)
			throws ClassNotFoundException {
		try {
			if (parentCL == null)
				throw new ClassNotFoundException();
			return parentCL.loadClass(name);
		} catch (ClassNotFoundException e) {
			if (history.containsKey(name))
				return history.get(name);
			String relativeClassPath = name.replaceAll("\\.", "\\"
					+ File.separator);
			FileInputStream classReader = null;
			try {
				File classFile = new File(projectClassPath
						+ File.separator + relativeClassPath + ".class");
				//TODO if classFile.exists();
				classReader = new FileInputStream(classFile);
				if (ignoredTests.contains(name)) {
					byte[] classBytecode = readClassFile(classFile);
					Class<?> classDef = defineClass(name, classBytecode, 0, classBytecode.length);
					history.put(name, classDef);
					return classDef;
				} else {
					synchronized (this) {
						CachedClassFileResult result = CachedClassFilesManager.getClass(name);
						if (result.isWrote) {
							
							byte[] classBytecode = readClassFile(result.classFile);
							Class<?> classDef = defineClass(name, classBytecode, 0, classBytecode.length);
							history.put(name, classDef);
							return classDef;
						} else {
							byte classBytecode[] = instrumentationModule.tracePredicatesFromClass(projectClassPath, name);
							FileOutputStream cachedClassFile = new FileOutputStream(result.classFile);
							cachedClassFile.write(classBytecode, 0, classBytecode.length);
//							if (name.equals("com.google.inject.spi.InjectionPoint")) {
//								System.out.println("CUSTOM CL");
//								for (Entity en : instrumentationModule.getTracedPredicates())
//									System.out.println(((Predicate)en.context).id);
//							}
							cachedClassFile.close();
							logger.addAllEntities(instrumentationModule.getTracedPredicates());
							Class<?> classDef = defineClass(name, classBytecode, 0, classBytecode.length);
							history.put(name, classDef);
							return classDef;
						}
					}
				}
			} catch (IOException ioException) {
				for (ImprovedJarReader jar : jarsInCP) {
					try {
						byte[] classBytes = jar.getResource(relativeClassPath.replaceAll("\\\\", "/") + ".class");
						if (classBytes != null) {
							Class<?> c = defineClass(name, classBytes, 0, classBytes.length);
							history.put(name, c);
							return c;
						}
					} catch (Exception e2) {
						// not in this jar
					}
				}
				throw new ClassNotFoundException("Can't find class " + name);
			}

		}
	}

	private byte[] readClassFile(File classFileName) throws IOException {
		FileInputStream classFile = new FileInputStream(classFileName);
		
		ArrayList<Byte> classContent = new ArrayList<Byte>();
		
		byte buff[] = new byte[512];
		int len;
		while ( (len = classFile.read(buff)) > 0)
			for (int j = 0; j < len; j++)
				classContent.add(buff[j]);
			
		int classSize = classContent.size();
		byte classBytecode[] = new byte[classSize];
		 
		for (int i = 0; i < classSize; i++)
			classBytecode[i] = classContent.get(i);
		classFile.close();
		
		return classBytecode;
	}

	public void clearHistory() {
		CachedClassFilesManager.clearHistory();
		jarsInCP = new ArrayList<ImprovedJarReader>();
		history = new HashMap<String, Class<?>>();
		lastProject = "";
		System.out.println("CLEARED");
	}
	
	@Override
	public URL getResource(String name) {
		URL res =  super.getResource(name);
		if (res == null) {
			File f = new File(projectClassPath, name);
			if  (f.exists())
				try {
					res = f.toURI().toURL();
				} catch (MalformedURLException e) {
					//??
				}
		}
		return res;
	}

} 
